using System;
using System.Collections;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using Microsoft.SPOT;
using Microsoft.SPOT.Hardware;

namespace DuinoWebserver
{
    public enum HttpRequestMethod
    {
        Unknown,
        Get,
        Post,
        Put,
        Delete,
        Options,
        Move,
        Any
    }

    /// <summary>
    /// Holds information about a web request
    /// </summary>
    /// <remarks>
    /// Will expand as required, but stay simple until needed.
    /// </remarks>
    public class Request : IDisposable
    {
        /// <summary>
        /// URL of request
        /// </summary>
        public string Url { get; private set; }
        
        /// <summary>
        /// HTTP Request method
        /// </summary>
        public HttpRequestMethod Method { get; private set; }

        public Socket ClientSocket { get; private set; }

        public Hashtable Header { get; private set; }

        private const int BUFFER_SIZE = 4096; //8192; //1536;
        private IPAddress _initialIPAdress = IPAddress.Any;

        internal Request(Socket clientSocket, char[] headerData)
        {
            Header = new Hashtable();
            ClientSocket = clientSocket;
            
            // this is for check if client is still available
            _initialIPAdress = ClientIPAddress;
            
            ProcessRequest(headerData);
        }

        /// <summary>
        /// Client IP address
        /// </summary>
        public IPAddress ClientIPAddress
        {
            get
            {
                IPEndPoint ip = ClientSocket.RemoteEndPoint as IPEndPoint;
                if (ip != null) return ip.Address;
                return null;
            }
        }

        /// <summary>
        /// Send a response back to the client
        /// </summary>
        /// <param name="response"></param>
        public void SendResponse(string response)
        {
            SendResponse(response, "text/html");
        }
 
        public void SendResponse(string response, string contentType)
        {
            if (ClientSocket != null)
            {
                var contentLength = response.Length.ToString();
                string header = "HTTP/1.0 200 OK\r\n"
                    + "Content-Type: " + contentType + "; charset=utf-8\r\n"
                    + "Content-Length: " + contentLength + "\r\n"
                    + "Connection: close\r\n"
                    + "\r\n";
                TrySend(Encoding.UTF8.GetBytes(header), header.Length, SocketFlags.None);
                TrySend(Encoding.UTF8.GetBytes(response), response.Length, SocketFlags.None);

                Debug.Print("Response data "
                    + " Content-Length: " + contentLength
                    + "; Content-Type: " + contentType + ".");
            }
        }

        /// <summary>
        /// Send a options response back to the client
        /// </summary>
        /// <param name="response"></param>
        public void SendOptionsResponse()
        {
            if (ClientSocket != null)
            {
                string cmds = "GET, POST, PUT, DELETE, MOVE";
                var dat = Encoding.UTF8.GetBytes(cmds);
                string header = "HTTP/1.1 200 OK\r\n"
                    + "Content-Length: " + dat.Length + "\r\n"
                    + "Allow: " + cmds + "\r\n"
                    + "Connection: close\r\n"
                    + "\r\n";
                TrySend(Encoding.UTF8.GetBytes(header), header.Length, SocketFlags.None);
                TrySend(dat, dat.Length, SocketFlags.None);
                //ClientSocket.Send(Encoding.UTF8.GetBytes(header));
                //ClientSocket.Send(dat);
            }
        }

        private StringBuilder _stringBuilder = new StringBuilder();

        public void SendHeader(int length)
        {SendHeader(length, "text/html");
        }
        public void SendHeader(int length, string contentType )
        {
            if (ClientSocket != null)
            {
                var contentLength = length <= 0 ? null : "Content-Length: " + length.ToString() + "\r\n";
                string header = "HTTP/1.0 200 OK\r\n"
                    + "Content-Type: " + contentType + "; charset=utf-8\r\n"
                    + contentLength
                    + "Connection: close\r\n"
                    + "\r\n";
                TrySend(Encoding.UTF8.GetBytes(header), header.Length, SocketFlags.None);
                Debug.Print("Begin response "
                    + " Content-Length: " + length.ToString()
                    + "; Content-Type: " + contentType + ".");

                _stringBuilder = new StringBuilder();
            }
        }

        private void _GC()
        {
            _GC(true);
        }

        private void _GC(bool printSize)
        {
            var size = Microsoft.SPOT.Debug.GC(true);
            if (printSize)
                Microsoft.SPOT.Debug.Print("GC free size: " + size.ToString());
        }

        public void Append(string value)
        {
            _GC(false);
            if (ClientSocket != null && _stringBuilder != null)
            {
                _stringBuilder.Append(value);
                if (_stringBuilder.Length > 1024)
                {
                    var chunk = _stringBuilder.ToString();
                    _stringBuilder.Remove(0, _stringBuilder.Length);
                    TrySend(Encoding.UTF8.GetBytes(chunk), chunk.Length, SocketFlags.None);
                    _GC();
                    Debug.Print("Append response.");
                }
            }
        }

        public void Flush()
        {
            _GC();
            if (ClientSocket != null && _stringBuilder != null)
            {
                if (_stringBuilder.Length > 0)
                {
                    var chunk = _stringBuilder.ToString();
                    _stringBuilder.Remove(0, _stringBuilder.Length);
                    _stringBuilder = null;
                    var data = Encoding.UTF8.GetBytes(chunk);

                    TrySend(data, chunk.Length, SocketFlags.None);
                }
                Debug.Print("Flush response.");
            }
        }

        /// <summary>
        /// Sends a file back to the client
        /// </summary>
        /// <remarks>
        /// Assumes the application using this has checked whether it exists
        /// </remarks>
        /// <param name="filePath"></param>
        public void SendFile(string filePath)
        {
            string contentType = getContentType(filePath);
            string contentLength;
            using (FileStream inputStream = new FileStream(filePath, FileMode.Open))
            {
                // Send the header
                contentLength = inputStream.Length.ToString();
                string header = "HTTP/1.0 200 OK\r\nContent-Type: " + contentType /*+ "; charset=utf-8"*/
                    + "\r\nContent-Length: " + contentLength
                    + "\r\nConnection: close\r\n\r\n";
                TrySend(Encoding.UTF8.GetBytes(header), header.Length, SocketFlags.None);

                byte[] readBuffer = new byte[BUFFER_SIZE];
                while (true)
                {
                    // Send the file a few bytes at a time
                    int bytesRead = inputStream.Read(readBuffer, 0, readBuffer.Length);
                    if (bytesRead == 0)
                        break;

                    TrySend(readBuffer, bytesRead, SocketFlags.None);
                }
            }
            Debug.Print("Sent file " + filePath 
                + "; Content length: " + contentLength
                + "; Content type: " + contentType + ".");
        }

        /// <summary>
        /// Map the file extension to a mime type
        /// </summary>
        /// <param name="filePath">The file for get mime type</param>
        /// <returns>The mime type or String.Empty if type is unknown</returns>
        private static string getContentType(string filePath)
        {
            string contentType = String.Empty;

            int dot = filePath.LastIndexOf('.');
            if (dot == 0) return contentType;

            switch (filePath.Substring(dot + 1).ToLower())
            {
                case "css":
                    contentType = "text/css";
                    break;
                case "xml":
                case "xsl":
                    contentType = "text/xml";
                    break;
                case "htm":
                case "html":
                    contentType = "text/html";
                    break;
                case "js":
                case "json":
                    contentType = "text/js";
                    break;
                case "jpg":
                case "jpeg":
                    contentType = "image/jpeg";
                    break;
                case "gif":
                    contentType = "image/gif";
                    break;
                case "png":
                    contentType = "image/png";
                    break;
                // Not exhaustive. Extend this list as required.
            }
            return contentType;
        }

        /// <summary>
        /// Convert HttpRequestMethod to string.
        /// </summary>
        /// <param name="method">The HTTP request method.</param>
        /// <returns>The string of the HTTP request method.</returns>
        public static string MethodToString(HttpRequestMethod method)
        {
            switch (method)
            {
                case HttpRequestMethod.Unknown:
                    return "Unknown";
                case HttpRequestMethod.Get:
                    return "GET";
                case HttpRequestMethod.Post:
                    return "POST";
                case HttpRequestMethod.Put:
                    return "PUT";
                case HttpRequestMethod.Delete:
                    return "DELETE";
                case HttpRequestMethod.Options:
                    return "OPTIONS";
                case HttpRequestMethod.Any:
                    return "ANY";
                case HttpRequestMethod.Move:
                    return "MOVE";
                default:
                    return "undefined";
            }
        }

        /// <summary>
        /// Try send buffer data to ClientSocket.
        /// </summary>
        /// <param name="buffer">The buffer.</param>
        /// <param name="size">The buffer size.</param>
        /// <param name="socketFlags">The socket flags.</param>
        private void TrySend(byte[] buffer, int size, SocketFlags socketFlags)
        {
            if (_initialIPAdress.ToString() != ClientIPAddress.ToString())
#if MF_FRAMEWORK
                throw new SocketException(SocketError.ConnectionAborted); 
#else
                throw new SocketException((int)SocketError.ConnectionAborted);
#endif

                // check if socket is still connected
            // see: http://www.eggheadcafe.com/microsoft/NET-Microframework/34067547/using-sockets.aspx
            if (!ClientSocket.Poll(- 1, SelectMode.SelectWrite))
            {
                // ConnectionAborted is Error Code=10053; see:
                // http://blogs.msdn.com/b/davidklinems/archive/2004/11/04/252689.aspx

#if MF_FRAMEWORK
                throw new SocketException(SocketError.ConnectionAborted); 
#else
                throw new SocketException((int)SocketError.ConnectionAborted);
#endif
            }
            ClientSocket.Send(buffer, size, socketFlags);
        }

        public string ReceiveData()
        {
            var contentLength = Header["Content-Length"].ToString() ?? String.Empty;
            var contentType = Header["Content-Type"].ToString() ?? String.Empty;
            var length = int.Parse(contentLength);

//            var bufferSize = 256;
            var buffer = new byte[BUFFER_SIZE];
            var receiveBuffer = new byte[0];

            while (length > 0)
            {
                // for delayed data receive
                int availableBytes = WebServer.WaitForSocketAvailable(ClientSocket);

                int size = (availableBytes > BUFFER_SIZE ? BUFFER_SIZE : availableBytes);
                size = (size > length ? length : size);

                var received = ClientSocket.Receive(buffer, size, SocketFlags.None);
                if (received > 0)
                {
                    receiveBuffer = Utility.CombineArrays(receiveBuffer, 
                        Utility.ExtractRangeFromArray(buffer, 0, received));
                    length -= received;
                }
            }
            Debug.Print("Received data "
                + " Content-Length: " + contentLength
                + "; Content-Type: " + contentType + ".");

            var result = Encoding.UTF8.GetChars(receiveBuffer);
            return new string(result);
        }

        /// <summary>
        /// Receives a file from the client
        /// </summary>
        /// <remarks>
        /// Assumes the application using this has checked for:
        /// - file does not exist 
        /// - file directory exists.
        /// </remarks>
        /// <param name="filePath">file path to store received data stream</param>
        public void StoreFile(string filePath)
        {
            var contentLength = Header["Content-Length"].ToString() ?? String.Empty;
            var length = int.Parse(contentLength);
            var bufferSize = 256;
            var buffer = new byte[bufferSize];

            using (FileStream fileStream = new FileStream(filePath, FileMode.Create))
            {
                while (length > 0)
                {
                    // for delayed data receive
                    int availableBytes = WebServer.WaitForSocketAvailable(ClientSocket);

                    int size = (availableBytes > bufferSize ? bufferSize : availableBytes);
                    size = (size > length ? length : size);

                    var received = ClientSocket.Receive(buffer, size, SocketFlags.None);
                    if (received > 0)
                    {
                        fileStream.Write(buffer, 0, received);
                        length -= received;
                    }
                }
                fileStream.Flush();
            }
            Debug.Print("Stored file " + filePath
                + "; Content-Length: " + contentLength + ".");
        }

        /// <summary>
        /// Send a Not Found response
        /// </summary>
        public void SendNotFoundResponse()
        {
            string header = "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
            if (ClientSocket != null)
                TrySend(Encoding.UTF8.GetBytes(header), header.Length, SocketFlags.None);
            
            Debug.Print("Sent: 404 Not Found response.");
        }

        /// <summary>
        /// Send a No Content response
        /// </summary>
        public void SendNoContentResponse()
        {
            string header = "HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
            if (ClientSocket != null)
                TrySend(Encoding.UTF8.GetBytes(header), header.Length, SocketFlags.None);

            Debug.Print("Sent: 204 No Content response.");
        }

        /// <summary>
        /// Process the request header
        /// </summary>
        /// <param name="headerData"></param>
        private void ProcessRequest(char[] headerData)
        {
            string content = new string(headerData);
            Debug.Print(content);
            var headerLines = content.Split('\n');

            // Parse the first line of the request, e.g. "GET /path/ HTTP/1.1"
            string firstLine = headerLines[0];
            string[] words = firstLine.Split(' ');
            var method = words[0];

            // Uri.UnescapeDataString don't exists in NETMF
            Url = UnescapeString(words[1]);

            switch (method)
            {
                case "GET":
                    Method = HttpRequestMethod.Get;
                    break;
                case "POST":
                    Method = HttpRequestMethod.Post;
                    break;
                case "PUT":
                    Method = HttpRequestMethod.Put;
                    break;
                case "DELETE":
                    Method = HttpRequestMethod.Delete;
                    break;
                case "OPTIONS":
                    Method = HttpRequestMethod.Options;
                    break;
                case "MOVE":
                    Method = HttpRequestMethod.Move;
                    break;
                default:
                    Method = HttpRequestMethod.Unknown;
                    break;
            }

            // look for further headers in other lines of the request (e.g. User-Agent, Cookie)
            foreach (var line in headerLines)
            {
                if (line.IndexOf(':') < 0)
                    continue;

                var lineParts = line.Split(':');

                var key = lineParts[0].Trim(' ', '\t');                
                var value = lineParts[1].Trim(' ', '\t', '\r');
                if (!Header.Contains(key))
                    Header.Add(key, value);
            }
        }
        string UnescapeString(string s)
        {
            int idx = s.IndexOf('%'), beg = 0;
            if (idx < 0) return s;
            var sb = new System.Text.StringBuilder();
            var bytes = new System.Collections.ArrayList();
            while (idx >= 0 && idx < s.Length - 2)
            {
                if(idx > beg) sb.Append(s.Substring(beg, idx - beg));
                do
                {
                    var hex = s.Substring(idx + 1, 2);
                    byte val;
                    if (TryParseHex(hex, out val))
                    {
                        bytes.Add(val);
                        idx += 3;
                    }
                    else
                    {
                        idx++;
                    }
                } while (idx < s.Length - 2 && s[idx] == '%');
                var bs = (byte[])bytes.ToArray(typeof(byte));
                sb.Append(Encoding.UTF8.GetChars(bs));//.GetString(bs));
                bytes.Clear();
                beg = idx;
                idx = s.IndexOf('%', idx);
            }
            if (beg < s.Length) sb.Append(s.Substring(beg));
            return sb.ToString();
        }
        int ParseHex(string s)
        {
            int val = 0;
            int res = 0;
            for (int i = 0; i < s.Length; i++)
            {
                res *= 16;
                char c = s[i];
                if (c >= '0' && c <= '9')
                {
                    val = c - '0';
                }
                else
                {
                    switch (c)
                    {
                        case 'a':
                        case 'A': val = 10;
                            break;
                        case 'b':
                        case 'B': val = 11;
                            break;
                        case 'c':
                        case 'C': val = 12;
                            break;
                        case 'd':
                        case 'D': val = 13;
                            break;
                        case 'e':
                        case 'E': val = 14;
                            break;
                        case 'f':
                        case 'F': val = 15;
                            break;
                        default: throw new ArgumentException("Not a Hex-Char: " + c);
                    }
                }
                res += val;
            }
            return res;
        }
        bool TryParseHex(string s, out byte b)
        {
            int val = 0;
            int res = 0;
            for (int i = 0; i < s.Length; i++)
            {
                res *= 16;
                char c = s[i];
                if (c >= '0' && c <= '9')
                {
                    val = c - '0';
                }
                else if (c >= 'a' && c <= 'f')
                {
                    val = c - 'a' + 10;
                }
                else if (c >= 'A' && c <= 'F')
                {
                    val = c - 'A' + 10;
                }
                else
                {
                    b = 0;
                    return false;
                }
                res += val;
            }
            b = (byte)res;
            return true;
        }

        #region IDisposable Members

        public void Dispose()
        {
            if (ClientSocket != null)
            {
                ClientSocket.Close();
                ClientSocket = null;
            }
        }

        #endregion
    }
}
